var path = require("path");
var p = require("pfork");
var fse = require("fs-extra2");
var http = require("http");
var LRU = require("lru-cache");
var extend = require("extend");
var EventEmitter = require("events").EventEmitter;
var pluginMgr = new EventEmitter();
var colors = require("colors/safe");
var util = require("../util");
var logger = require("../util/logger");
var pluginUtil = require("./util");
var config = require("../config");
var getPluginsSync = require("./get-plugins-sync");
var getPlugin = require("./get-plugins");
var rulesMgr = require("../rules");
var RulesMgr = require("../rules/rules");
var properties = require("../rules/util").properties;
var httpMgr = require("../util/http-mgr");
var protocols = require("../rules/protocols");
var common = require("../util/common");
var mp = require("./module-paths");
var createSharedStorage = require("../plugins/shared-storage");

var encodeURIComponent = util.encodeURIComponent;
var RULES_TPL_RE =
	/^[^\n\r\S]*(``)[^\n\r\S]*((?:_?var(\.[^\s=]+)?|whistle|rule)\.tpl)[^\n\r\S]*[\r\n]([\s\S]+?)[\r\n][^\n\r\S]*\1\s*$/gm;
var REMOTE_RULES_RE =
	/^\s*@(`?)(whistle\.[a-z\d_\-]+(?:\/[^\s#]*)?|(?:https?:\/\/|[a-z]:[\\/]|~?\/)[^\s#]+|\$(?:whistle\.)?[a-z\d_-]+[/:][^\s#]+)\s*?\1(?:#.*)?$/im;
var PLUGIN_MAIN = path.join(__dirname, "./load-plugin");
var PIPE_PLUGIN_RE = /^pipe:\/\/(?:whistle\.|plugin\.)?([a-z\d_\-]+)(?:\(([\s\S]*)\))?$/;
var RULE_VALUE_HEADER = "x-whistle-rule-value";
var SNI_VALUE_HEADER = "x-whistle-sni-value";
var GLOBAL_PLUGIN_VARS_HEAD = "x-whistle-global-plugin-vars" + config.uid;
var PLUGIN_VARS_HEAD = "x-whistle-plugin-vars" + config.uid;
var RULE_URL_HEADER = "x-whistle-rule-url";
var ETAG_HEADER = "x-whistle-etag";
var MAX_AGE_HEADER = "x-whistle-max-age";
var FULL_URL_HEADER = "x-whistle-full-url";
var REAL_URL_HEADER = "x-whistle-real-url";
var RELATIVE_URL_HEADER = "x-whistle-relative-url";
var EXTRA_URL_HEADER = "x-whistle-extra-url";
var UI_REQUEST_HEADER = "x-whistle-auth-ui-request";
var REQ_ID_HEADER = "x-whistle-req-id";
var PIPE_VALUE_HEADER = "x-whistle-pipe-value";
var CUSTOM_PARSER_HEADER = "x-whistle-frame-parser";
var STATUS_CODE_HEADER = "x-whistle-status-code";
var PLUGIN_REQUEST_HEADER = "x-whistle-plugin-request";
var LOCAL_HOST_HEADER = "x-whistle-local-host";
var PROXY_VALUE_HEADER = "x-whistle-proxy-value";
var PAC_VALUE_HEADER = "x-whistle-pac-value";
var METHOD_HEADER = "x-whistle-method";
var SHOW_LOGIN_BOX = "x-whistle2-show-login-box." + config.uid;
var FROM_TUNNEL_HEADER = "x-whistle-from-tunnel-req-" + config.uid;
var CLIENT_PORT_HEAD = config.CLIENT_PORT_HEAD;
var HOST_IP_HEADER = "x-whistle-host-ip";
var GLOBAL_VALUE_HEAD = "x-whistle-global-value";
var SERVER_NAME_HEAD = "x-whistle-server-name";
var COMMON_NAME_HEAD = "x-whistle-common-name";
var CERT_CACHE_INFO = "x-whistle-cert-cache-info";
var STATUS_ERR = new Error("Non 200");
var INTERVAL = 6000;
var CHECK_INTERVAL = 1000 * 60 * 60 * 2;
var MAX_CERT_SIZE = 72 * 1024;
var portsField = typeof Symbol === "undefined" ? "_ports" : Symbol("_ports"); // eslint-disable-line
var notLoadPlugins = config.networkMode || config.rulesOnlyMode;
var allPlugins = notLoadPlugins ? {} : getPluginsSync();
var authPlugins = [];
var tunnelKeys = [];
var LOCALHOST = "127.0.0.1";
var MAX_RULES_LENGTH = 1024 * 256;
var rulesCache = new LRU({ max: 36 });
var CUSTOM_CERT_HEADER = config.CUSTOM_CERT_HEADER;
var ENABLE_CAPTURE_HEADER = config.ENABLE_CAPTURE_HEADER;
var PLUGIN_HOOKS = config.PLUGIN_HOOKS;
var PLUGIN_HOOK_NAME_HEADER = config.PLUGIN_HOOK_NAME_HEADER;
var UPGRADE_HEADER = config.UPGRADE_HEADER;
var HTTP_RE = /^https?:\/\/./;
var conf = {};
var EXCLUDE_CONF_KEYS = {
	uid: 1,
	INTERNAL_ID: 1,
	SNI_PLUGIN_HEADER: 1,
	WEBUI_HEAD: 1,
	CLIENT_INFO_HEAD: 1,
	COMPOSER_CLIENT_ID_HEADER: 1,
	TEMP_TUNNEL_DATA_HEADER: 1
};
var EXCLUDE_NAMES = {
	password: 1,
	shadowRules: 1,
	rules: 1,
	values: 1
};
var debugMode = config.debugMode;
var whistleProxy;
var REGISTRY_LIST = path.join(common.getDefaultWhistlePath(), ".registry.list");
var MAX_REG_COUNT = 100;
var storage = createSharedStorage(REGISTRY_LIST);
var registryList = filterRegList(common.readJsonSync(REGISTRY_LIST));

function filterRegList(l) {
	if (!Array.isArray(l)) {
		l = [];
	}
	return l
		.filter(function (url) {
			return common.getRegistry(url);
		})
		.slice(0, MAX_REG_COUNT);
}

function readRegList() {
	fse.readJson(REGISTRY_LIST, function (e, list) {
		if (!e) {
			registryList = filterRegList(list);
		}
	});
}

if (notLoadPlugins) {
	getPlugin = function (cb) {
		cb({});
	};
}

pluginMgr.addRegistry = function (registry) {
	registry = common.getRegistry(registry);
	if (registry && registryList.indexOf(registry) === -1) {
		registryList.push(registry);
		storage.setAll(registryList);
	}
};

/*eslint no-console: "off"*/
Object.keys(config).forEach(function (name) {
	if (EXCLUDE_NAMES[name]) {
		return;
	}
	var value = config[name];
	if (name === "passwordHash") {
		conf.password = value;
	}
	if (name === "globalData") {
		conf.globalData = value;
	} else if (!EXCLUDE_CONF_KEYS[name]) {
		var type = typeof value;
		if (type == "string" || type == "number" || type === "boolean") {
			conf[name] = value;
		}
	}
});
conf.client = config.client;
conf.PLUGIN_HOOKS = config.PLUGIN_HOOKS;
conf.uiHostList = config.uiHostList;
var pluginHostMap = config.pluginHostMap;
var pluginHosts = (conf.pluginHosts = {});
if (pluginHostMap) {
	Object.keys(pluginHostMap).forEach(function (host) {
		var name = pluginHostMap[host];
		var list = (pluginHosts[name] = pluginHosts[name] || []);
		list.push(host);
	});
}

pluginMgr.REGISTRY_LIST = REGISTRY_LIST;

pluginMgr.getPluginsPath = mp.getPaths;

pluginMgr.getRegistryList = function () {
	return registryList;
};

pluginMgr.getTunnelKeys = function () {
	return tunnelKeys;
};

pluginMgr.on("updateRules", function (byParse) {
	rulesMgr.clearAppend();
	authPlugins = [];
	tunnelKeys = [];
	var ruleTpls;
	Object.keys(allPlugins)
		.sort(function (a, b) {
			var p1 = allPlugins[a];
			var p2 = allPlugins[b];
			return (
				util.compare(p1.priority, p2.priority) ||
				util.compare(p2.mtime, p1.mtime) ||
				(a > b ? 1 : 0)
			);
		})
		.forEach(function (name) {
			if (pluginIsDisabled(name.slice(0, -1))) {
				return;
			}
			var plugin = allPlugins[name];
			var rules = plugin.rules;
			if (plugin.enableAuthUI) {
				authPlugins.push(plugin);
			}
			if (plugin.tunnelKey) {
				plugin.tunnelKey.forEach(function (key) {
					if (tunnelKeys.indexOf(key) === -1) {
						tunnelKeys.push(key);
					}
				});
			}
			if (rules) {
				rules = rules.replace(REMOTE_RULES_RE, function (_, apo, rulesUrl) {
					return util.getRemoteRules(apo, rulesUrl);
				});
				rules = rules.replace(RULES_TPL_RE, function (_, __, key, subVar, value) {
					ruleTpls = ruleTpls || {};
					var simpleName = name.slice(0, -1);
					var isPrivate = key[0] === "_";
					if (isPrivate || key[0] === "v") {
						key = (isPrivate ? "_" : "") + "%" + simpleName + (subVar || "");
					} else if (key === "whistle.tpl") {
						key = "whistle." + simpleName;
					} else {
						key = simpleName;
					}
					if (!ruleTpls[key]) {
						ruleTpls[key] = value;
					}
					return "";
				});
				rulesMgr.append(rules, plugin.path, true);
			}
		});
	pluginMgr.ruleTpls = ruleTpls;
	if (byParse !== true) {
		httpMgr.triggerChange();
	}
});

pluginMgr.updateRules = function () {
	pluginMgr.emit("updateRules");
};

pluginMgr.loadCert = function (req, plugin, callback) {
	loadPlugin(plugin, function (err, ports) {
		if (err || !ports || !ports.sniPort) {
			return callback();
		}
		var options = getOptions(req);
		options.maxLength = MAX_CERT_SIZE;
		options.headers[PLUGIN_HOOK_NAME_HEADER] = PLUGIN_HOOKS.SNI;
		options.headers[SERVER_NAME_HEAD] = encodeURIComponent(req.serverName);
		options.headers[COMMON_NAME_HEAD] = encodeURIComponent(req.commonName);
		if (!req.useSNI || req.isHttpsServer) {
			options.headers[config.SNI_TYPE_HEADER] = req.isHttpsServer ? "1" : "0";
		}
		if (req.sniRuleValue) {
			options.headers[SNI_VALUE_HEADER] = encodeURIComponent(req.sniRuleValue);
		}
		if (req.hasCertCache) {
			options.headers[CERT_CACHE_INFO] = req.hasCertCache;
		}
		options.port = ports.sniPort;
		requestPlugin(options, function (err, body, res) {
			if (err || res.statusCode !== 200) {
				return callback(err || STATUS_ERR);
			}
			if (!body) {
				return callback();
			}
			if (body === "false") {
				return callback(false);
			}
			if (body === "true") {
				return callback(true);
			}
			try {
				body = JSON.parse(body);
				if (!body || !body.name || !util.isString(body.key) || !util.isString(body.cert)) {
					return callback();
				}
				callback({
					key: body.key,
					cert: body.cert,
					name: body.name,
					mtime: body.mtime > 0 ? body.mtime : undefined
				});
			} catch (e) {
				callback(e);
			}
		});
	});
};

pluginMgr.on("update", function (result) {
	Object.keys(result).forEach(function (name) {
		pluginMgr.stopPlugin(result[name]);
	});
	readRegList();
});
pluginMgr.on("uninstall", function (result) {
	Object.keys(result).forEach(function (name) {
		pluginMgr.stopPlugin(result[name]);
	});
});

function showVerbose(oldData, newData) {
	if (!debugMode) {
		return;
	}
	var uninstallData, installData, updateData;
	Object.keys(oldData).forEach(function (name) {
		var oldItem = oldData[name];
		var newItem = newData[name];
		if (!newItem) {
			uninstallData = uninstallData || {};
			uninstallData[name] = oldItem;
		} else if (newItem.path != oldItem.path || newItem.mtime != oldItem.mtime) {
			updateData = updateData || {};
			updateData[name] = newItem;
		}
	});

	Object.keys(newData).forEach(function (name) {
		if (!oldData[name]) {
			installData = installData || {};
			installData[name] = newData[name];
		}
	});

	uninstallData &&
		Object.keys(uninstallData).forEach(function (name) {
			console.log(
				colors.red(
					util.formatDate(new Date(uninstallData[name].mtime)) +
						" [uninstall plugin] " +
						name.slice(0, -1)
				)
			);
		});
	installData &&
		Object.keys(installData).forEach(function (name) {
			console.log(
				colors.green(
					util.formatDate(new Date(installData[name].mtime)) +
						" [install plugin] " +
						name.slice(0, -1)
				)
			);
		});
	updateData &&
		Object.keys(updateData).forEach(function (name) {
			console.log(
				colors.yellow(
					util.formatDate(new Date(updateData[name].mtime)) +
						" [update plugin] " +
						name.slice(0, -1)
				)
			);
		});
}

function readReqRules(dir, callback) {
	pluginUtil.readFile(path.join(path.join(dir, "_rules.txt")), function (err, rulesText) {
		if (err) {
			pluginUtil.readFile(path.join(path.join(dir, "reqRules.txt")), function (_, rulesText) {
				callback(util.trim(rulesText));
			});
			return;
		}
		callback(util.trim(rulesText));
	});
}

function readJson(pkgPath, callback) {
	fse.readJson(pkgPath, function (err, json) {
		if (!err) {
			return callback(err, json);
		}
		fse.readJson(pkgPath, callback);
	});
}

function readPackages(obj, callback) {
	var _plugins = {};
	var count = 0;
	var callbackHandler = function () {
		if (--count <= 0) {
			callback(_plugins);
		}
	};
	Object.keys(obj).forEach(function (name) {
		var pkg = allPlugins[name];
		var newPkg = obj[name];
		if (!pkg || pkg.path != newPkg.path || pkg.mtime != newPkg.mtime) {
			++count;
			readJson(path.join(newPkg.path, "package.json"), function (_, result) {
				if (result && result.version && pluginUtil.isPluginName(result.name)) {
					var conf = result.whistleConfig || "";
					var tabs = util.getInspectorTabs(conf);
					var hintList = util.getHintList(conf);
					var simpleName = name.slice(0, -1);
					newPkg.enableAuthUI = !!conf.enableAuthUI;
					newPkg.noOpt = !!(
						conf.noOption ||
						conf.notOption ||
						conf.disableOption ||
						conf.disabledOption
					);
					newPkg.updateUrl = util.getUpdateUrl(conf);
					newPkg.inheritAuth = !!conf.inheritAuth;
					newPkg.tunnelKey = util.getTunnelKey(conf);
					newPkg.version = result.version;
					newPkg.staticDir = util.getStaticDir(conf);
					newPkg.priority =
						parseInt(conf.priority, 10) || parseInt(result.pluginPriority, 10) || 0;
					newPkg.rulesUrl = util.getCgiUrl(conf.rulesUrl);
					newPkg.valuesUrl = util.getCgiUrl(conf.valuesUrl);
					newPkg.networkColumn = util.getNetworkColumn(conf);
					newPkg.networkMenus = util.getPluginMenu(
						conf.networkMenus || conf.networkMenu,
						simpleName
					);
					newPkg.rulesMenus = util.getPluginMenu(
						conf.rulesMenus || conf.rulesMenu,
						simpleName
					);
					newPkg.valuesMenus = util.getPluginMenu(
						conf.valuesMenus || conf.valuesMenu,
						simpleName
					);
					newPkg.reqTab = util.getCustomTab(tabs.req, simpleName);
					newPkg.resTab = util.getCustomTab(tabs.res, simpleName);
					newPkg.tab = util.getCustomTab(tabs, simpleName);
					newPkg.toolTab = util.getCustomTab(conf.toolsTab || conf.toolTab, simpleName);
					newPkg.comTab = util.getCustomTab(conf.composerTab, simpleName);
					newPkg[util.PLUGIN_MENU_CONFIG] = util.getPluginMenuConfig(conf);
					newPkg[util.PLUGIN_INSPECTOR_CONFIG] = util.getPluginInspectorConfig(conf);
					newPkg.hintUrl = hintList ? undefined : util.getCgiUrl(conf.hintUrl);
					newPkg.hintList = hintList;
					newPkg.pluginVars = util.getPluginVarsConf(conf);
					newPkg.hideShortProtocol = !!conf.hideShortProtocol;
					newPkg.hideLongProtocol = !!conf.hideLongProtocol;
					newPkg.homepage = pluginUtil.getHomePageFromPackage(result);
					newPkg.description = result.description;
					newPkg.moduleName = result.name;
					newPkg.pluginHomepage = pluginUtil.getPluginHomepage(result);
					newPkg.openInPlugins = conf.openInPlugins ? 1 : undefined;
					newPkg.openInModal = util.getPluginModal(conf);
					newPkg.openExternal = conf.openExternal ? 1 : undefined;
					newPkg.registry = util.getRegistry(result);
					newPkg.latest = pkg && pkg.latest;
					_plugins[name] = newPkg;
					pluginUtil.readFile(
						path.join(path.join(newPkg.path, "rules.txt")),
						function (err, rulesText) {
							newPkg.rules = util.renderPluginRules(
								util.trim(rulesText),
								result,
								simpleName
							);
							readReqRules(newPkg.path, function (rulesText) {
								newPkg._rules = util.renderPluginRules(
									util.trim(rulesText),
									result,
									simpleName
								);
								pluginUtil.readFile(
									path.join(path.join(newPkg.path, "_values.txt")),
									function (err, rulesText) {
										newPkg[util.PLUGIN_VALUES] = pluginUtil.parseValues(
											util.renderPluginRules(rulesText, result, simpleName)
										);
										pluginUtil.readFile(
											path.join(path.join(newPkg.path, "resRules.txt")),
											function (err, rulesText) {
												newPkg.resRules = util.renderPluginRules(
													util.trim(rulesText),
													result,
													simpleName
												);
												pluginUtil.readWorker(
													newPkg.path,
													conf,
													function (hash) {
														newPkg.webWorker = hash;
														callbackHandler();
													}
												);
											}
										);
									}
								);
							});
						}
					);
				} else {
					callbackHandler();
				}
			});
		} else {
			_plugins[name] = pkg;
		}
	});

	if (count <= 0) {
		callback(_plugins);
	}
}

function checkUpdate(pluginNames) {
	pluginNames = pluginNames || Object.keys(allPlugins);
	var name = pluginNames.shift();
	var plugin;
	while (name) {
		if ((plugin = allPlugins[name]) && !plugin.isProj) {
			break;
		}
		name = pluginNames.shift();
	}
	if (name) {
		util.getLatestVersion(plugin, function (ver) {
			if (ver && plugin.version !== ver) {
				plugin.latest = ver;
			}
			checkUpdate(pluginNames);
		});
	} else {
		setTimeout(checkUpdate, CHECK_INTERVAL);
	}
}
setTimeout(checkUpdate, 5000);

var updating;
var updateTimer;
var delayTimer;

function refreshPlugins() {
	updating = true;
	delayTimer && clearTimeout(delayTimer);
	updateTimer && clearTimeout(updateTimer);
	delayTimer = null;
	updateTimer = null;
	getPlugin(function (result) {
		readPackages(result, function (_plugins) {
			var updatePlugins, uninstallPlugins;
			var pluginNames = Object.keys(allPlugins);
			pluginNames.forEach(function (name) {
				var plugin = allPlugins[name];
				var newPlugin = _plugins[name];
				if (!newPlugin) {
					uninstallPlugins = uninstallPlugins || {};
					uninstallPlugins[name] = plugin;
				} else if (newPlugin.path != plugin.path || newPlugin.mtime != plugin.mtime) {
					updatePlugins = updatePlugins || {};
					updatePlugins[name] = newPlugin;
				}
			});
			showVerbose(allPlugins, _plugins);
			allPlugins = _plugins;
			if (
				uninstallPlugins ||
				updatePlugins ||
				Object.keys(_plugins).length !== pluginNames.length
			) {
				uninstallPlugins && pluginMgr.emit("uninstall", uninstallPlugins);
				updatePlugins && pluginMgr.emit("update", updatePlugins);
				pluginMgr.emit("updateRules");
				pluginUtil.resetWorkers(allPlugins);
			}
			updating = false;
			update();
		});
	});
}

function update() {
	if (!config.inspectMode && !updating) {
		updateTimer = setTimeout(refreshPlugins, INTERVAL);
	}
}

update();

pluginMgr.refreshPlugins = function () {
	delayTimer = delayTimer || setTimeout(refreshPlugins, 200);
};

function getValue(rule) {
	if (!rule) {
		return;
	}
	var value = util.getMatcherValue(rule) || "";
	value += rule.port ? ":" + rule.port : "";
	return encodeURIComponent(value);
}

function addRealUrl(req, newHeaders) {
	var realUrl = req._realUrl;
	if (!realUrl) {
		var href = req.options && req.options.href;
		realUrl = util.isUrl(href) ? href : null;
	}
	if (realUrl && realUrl != req.fullUrl) {
		newHeaders[REAL_URL_HEADER] = encodeURIComponent(realUrl);
	}
	var rule = req.rules && req.rules.rule;
	if (rule) {
		if (rule.url !== rule.matcher) {
			var relPath = rule.url.substring(rule.matcher.length);
			newHeaders[RELATIVE_URL_HEADER] = encodeURIComponent(relPath);
		}
		if (rule.url) {
			newHeaders[RULE_URL_HEADER] = encodeURIComponent(rule.url);
		}
	}
}

function getPluginVars(value) {
	if (value) {
		try {
			value = JSON.stringify(value);
			return Buffer.from(value).toString("base64");
		} catch (e) {}
	}
}

function addPluginVars(req, headers, rule) {
	addRealUrl(req, headers);
	if (!rule) {
		delete headers[RULE_VALUE_HEADER];
		delete headers[GLOBAL_PLUGIN_VARS_HEAD];
		delete headers[PLUGIN_VARS_HEAD];
		return;
	}
	var plugin = rule.plugin;
	var name;
	var value;
	if (plugin) {
		name = plugin.moduleName.split(".", 2)[1];
		value = rule.value;
	} else {
		name = rule.matcher.split(":", 1)[0];
		value = util.getMatcherValue(rule);
	}
	if (value) {
		headers[RULE_VALUE_HEADER] = encodeURIComponent(value);
	} else {
		delete headers[RULE_VALUE_HEADER];
	}
	if (rule.rawPattern) {
		headers["x-whistle-raw-pattern_"] = encodeURIComponent(
			(rule.isRegExp ? 1 : 0) + "," + rule.rawPattern
		);
	}
	if (rule.url) {
		var extraUrl = rule.url;
		if (value) {
			extraUrl = rule.url.substring(value.length);
		}
		if (extraUrl) {
			headers[EXTRA_URL_HEADER] = encodeURIComponent(extraUrl);
		}
	}
	value = getPluginVars(req._globalPluginVars && req._globalPluginVars[name]);
	if (value) {
		headers[GLOBAL_PLUGIN_VARS_HEAD] = value;
	} else {
		delete headers[GLOBAL_PLUGIN_VARS_HEAD];
	}
	value = getPluginVars(req._pluginVars && req._pluginVars[name]);
	if (value) {
		headers[PLUGIN_VARS_HEAD] = value;
	} else {
		delete headers[PLUGIN_VARS_HEAD];
	}
}

function addPluginHeaders(req, headers, isKey) {
	if (req.reqId) {
		headers[REQ_ID_HEADER] = req.reqId;
	}
	if (req.fullUrl) {
		headers[FULL_URL_HEADER] = encodeURIComponent(req.fullUrl);
	}
	var clientIp = req.clientIp || util.getClientIp(req);
	if (clientIp) {
		headers[config.CLIENT_IP_HEAD] = clientIp;
	}
	var clientPort = req.clientPort || util.getClientPort(req);
	if (clientPort) {
		headers[config.CLIENT_PORT_HEAD] = clientPort;
	}
	if (req.fromTunnel) {
		headers[FROM_TUNNEL_HEADER] = "1";
	} else {
		delete headers[FROM_TUNNEL_HEADER];
	}
	headers[isKey ? "x-whistle-remote-address" : config.REMOTE_ADDR_HEAD] =
		req._remoteAddr || LOCALHOST;
	headers[isKey ? "x-whistle-remote-port" : config.REMOTE_PORT_HEAD] = req._remotePort || "0";
	if (req.fromComposer) {
		headers[config.REQ_FROM_HEADER] = "W2COMPOSER";
	} else {
		delete headers[config.REQ_FROM_HEADER];
	}
	return headers;
}

function addRuleHeaders(req, rules, headers, isPipe) {
	headers = headers || req.headers;
	addPluginHeaders(req, headers);
	if (req._isUIRequest) {
		headers[UI_REQUEST_HEADER] = "1";
		return headers;
	}
	rules = rules || "";
	var localHost = getValue(rules.host);
	if (localHost) {
		headers[LOCAL_HOST_HEADER] = localHost;
	}
	var rule = rules.rule;
	if (isPipe) {
		if (req._pipePlugin) {
			rule = rule || rulesMgr.resolveRule(req);
			var value;
			if (rule) {
				var name = req._pipePlugin.moduleName;
				name = name.substring(name.indexOf(".") + 1) + "://";
				if (rule.matcher.indexOf(name) === 0) value = util.getMatcherValue(rule);
			}
			rule = { plugin: req._pipePlugin, value: value };
		} else {
			rule = null;
		}
	}
	addPluginVars(req, headers, rule);
	var proxyRule = getValue(rules.proxy);
	if (proxyRule) {
		headers[PROXY_VALUE_HEADER] = proxyRule;
	}
	var pac = getValue(rules.pac);
	if (pac) {
		headers[PAC_VALUE_HEADER] = pac;
	}
	if (req._pipeValue) {
		headers[PIPE_VALUE_HEADER] = encodeURIComponent(req._pipeValue);
	}
	if (req.customParser) {
		headers[CUSTOM_PARSER_HEADER] = req.customParser;
	} else {
		delete headers[CUSTOM_CERT_HEADER];
	}
	if (req.globalValue) {
		headers[GLOBAL_VALUE_HEAD] = encodeURIComponent(req.globalValue);
	}
	return headers;
}

pluginMgr.addRuleHeaders = addRuleHeaders;

function loadPlugin(plugin, callback) {
	if (!plugin) {
		return callback(null, "");
	}
	var ports = plugin[portsField];
	if (ports) {
		return callback(null, ports);
	}
	util.getBoundIp(config.host, function (host) {
		conf.host = host || LOCALHOST;
		var moduleName = plugin.moduleName;
		var name = moduleName.substring(moduleName.indexOf("/") + 1);
		var isInline = config.inspectMode || process.env.PFORK_MODE === "inline";
		p.fork(
			{
				data: config.getPluginData(moduleName),
				_inspect: config.inspectMode,
				name: moduleName,
				script: PLUGIN_MAIN,
				value: plugin.path,
				isDev: plugin.isDev,
				version: plugin.version,
				staticDir: plugin.staticDir,
				CUSTOM_CERT_HEADER: CUSTOM_CERT_HEADER,
				ENABLE_CAPTURE_HEADER: ENABLE_CAPTURE_HEADER,
				RULE_VALUE_HEADER: RULE_VALUE_HEADER,
				SNI_VALUE_HEADER: SNI_VALUE_HEADER,
				GLOBAL_PLUGIN_VARS_HEAD: GLOBAL_PLUGIN_VARS_HEAD,
				PLUGIN_VARS_HEAD: PLUGIN_VARS_HEAD,
				RULE_URL_HEADER: RULE_URL_HEADER,
				MAX_AGE_HEADER: MAX_AGE_HEADER,
				ETAG_HEADER: ETAG_HEADER,
				FULL_URL_HEADER: FULL_URL_HEADER,
				REAL_URL_HEADER: REAL_URL_HEADER,
				RELATIVE_URL_HEADER: RELATIVE_URL_HEADER,
				EXTRA_URL_HEADER: EXTRA_URL_HEADER,
				REQ_ID_HEADER: REQ_ID_HEADER,
				PIPE_VALUE_HEADER: PIPE_VALUE_HEADER,
				CUSTOM_PARSER_HEADER: CUSTOM_PARSER_HEADER,
				STATUS_CODE_HEADER: STATUS_CODE_HEADER,
				PLUGIN_REQUEST_HEADER: PLUGIN_REQUEST_HEADER,
				LOCAL_HOST_HEADER: LOCAL_HOST_HEADER,
				HOST_VALUE_HEADER: LOCAL_HOST_HEADER,
				PROXY_VALUE_HEADER: PROXY_VALUE_HEADER,
				PAC_VALUE_HEADER: PAC_VALUE_HEADER,
				METHOD_HEADER: METHOD_HEADER,
				SHOW_LOGIN_BOX: SHOW_LOGIN_BOX,
				FROM_TUNNEL_HEADER: FROM_TUNNEL_HEADER,
				CLIENT_IP_HEADER: config.CLIENT_IP_HEAD,
				CLIENT_PORT_HEAD: CLIENT_PORT_HEAD,
				UI_REQUEST_HEADER: UI_REQUEST_HEADER,
				GLOBAL_VALUE_HEAD: GLOBAL_VALUE_HEAD,
				SERVER_NAME_HEAD: SERVER_NAME_HEAD,
				COMMON_NAME_HEAD: COMMON_NAME_HEAD,
				CERT_CACHE_INFO: CERT_CACHE_INFO,
				HOST_IP_HEADER: HOST_IP_HEADER,
				debugMode: debugMode,
				config: isInline ? extend(true, {}, conf) : conf // 防止 inline 时，子进程删除 conf
			},
			function (err, ports, child, first) {
				callback(err, ports);
				if (!first) {
					return;
				}
				if (err) {
					whistleProxy.emit("pluginLoadError", err, name, moduleName);
					logger.error(err);
					var mode = process.env.PFORK_MODE;
					if (debugMode || mode === "inline" || mode === "bind") {
						console.log(err);
					}
				} else {
					whistleProxy.emit("pluginLoad", child, name, moduleName);
					plugin[portsField] = ports;
					child.on("close", function () {
						delete plugin[portsField];
					});
				}
			}
		);
	});
}

pluginMgr.loadPlugin = loadPlugin;

pluginMgr.loadPluginByName = function (name, callback) {
	loadPlugin(getActivePluginByName(name), callback);
};

pluginMgr.stopPlugin = function (plugin) {
	p.kill(
		{
			script: PLUGIN_MAIN,
			value: plugin.path
		},
		10000
	);
};

pluginMgr.getPlugins = function () {
	return allPlugins;
};

function pluginIsDisabled(name) {
	if (config.notAllowedDisablePlugins) {
		return false;
	}
	if (properties.get("disabledAllPlugins")) {
		return true;
	}
	var disabledPlugins = properties.get("disabledPlugins") || {};
	return disabledPlugins[name];
}

function _getPlugin(protocol) {
	return pluginIsDisabled(protocol.slice(0, -1)) ? null : allPlugins[protocol];
}

pluginMgr.isDisabled = pluginIsDisabled;
pluginMgr.getPlugin = _getPlugin;

function getActivePluginByName(name) {
	return pluginIsDisabled(name) ? null : allPlugins[name + ":"];
}

rulesMgr.getPlugin = getActivePluginByName;

function getPluginByName(name) {
	return name && allPlugins[name + ":"];
}

pluginMgr.getPluginByName = getPluginByName;

function getPluginByRuleUrl(ruleUrl) {
	if (!ruleUrl || typeof ruleUrl != "string") {
		return;
	}
	var index = ruleUrl.indexOf(":");
	if (index == -1) {
		return null;
	}
	var protocol = ruleUrl.substring(0, index + 1);
	return pluginIsDisabled(protocol.slice(0, -1)) ? null : allPlugins[protocol];
}

pluginMgr.getPluginByRuleUrl = getPluginByRuleUrl;

function _loadPlugins(plugins, callback) {
	var rest = plugins.length;
	var results = [];
	var execCallback = function () {
		--rest === 0 && callback(results);
	};
	plugins.forEach(function (plugin, i) {
		loadPlugin(plugin, function (err, ports) {
			plugin.ports = ports;
			results[i] = ports || null;
			execCallback();
		});
	});
}

function loadPlugins(plugins, callback) {
	plugins = plugins.map(function (plugin) {
		return plugin.plugin;
	});
	_loadPlugins(plugins, callback);
}

pluginMgr.loadAuthPlugins = function (req, callback) {
	if (config.disableAuthUI || !authPlugins.length) {
		return callback();
	}
	req._isUIRequest = true;
	_loadPlugins(authPlugins, function (ports) {
		ports = ports.map(function (port, i) {
			return {
				authPort: port && port.authPort,
				plugin: authPlugins[i]
			};
		});
		var rest = ports.length;
		if (!rest) {
			return callback();
		}
		var options = getOptions(req);
		authReq(true, ports, req, options, function (forbidden) {
			if (forbidden) {
				if (req._redirectUrl || req._authHtmlUrl) {
					return callback(req._redirectUrl, null, req._authHtmlUrl);
				}
				var status = req._authStatus ? (req._showLoginBox ? 401 : 403) : 502;
				return callback(status, forbidden);
			}
			return callback();
		});
	});
};

function parseRulesList(req, results, isResRules) {
	var values = {};
	results = results.filter(emptyFilter);
	results.reverse().forEach(function (item) {
		extend(values, item.values);
	});
	var pluginRulesMgr = new RulesMgr(values);
	pluginRulesMgr.parse(results);
	if (isResRules) {
		req.curUrl = req.fullUrl;
		util.mergeRules(req, pluginRulesMgr.resolveRules(req), true);
	}
	return pluginRulesMgr;
}

function getPluginReqOpts(item, req, options, port) {
	var opts = extend({}, options);
	opts.headers = extend({}, options.headers);
	opts.port = port;
	addPluginVars(req, opts.headers, item);
	return opts;
}

function authReq(isReq, ports, req, options, callback) {
	if (!isReq) {
		return callback();
	}
	var rest = ports.length;
	var forbidden;
	var execCallback = function () {
		if (--rest === 0) {
			callback(forbidden);
		}
	};
	ports.forEach(function (item) {
		if (!item.authPort) {
			return execCallback();
		}
		options.headers[PLUGIN_HOOK_NAME_HEADER] = PLUGIN_HOOKS.AUTH;
		var opts = getPluginReqOpts(item, req, options, item.authPort);
		opts.maxLength = MAX_RULES_LENGTH;
		requestPlugin(opts, function (err, body, res) {
			var headers = res && res.headers;
			if (err || body) {
				if (!forbidden) {
					if (debugMode) {
						forbidden = err ? err.message || "Error" : body;
					} else {
						forbidden = err ? "Error" : body;
					}
					if (headers) {
						var authHtmlUrl = headers["x-auth-html-url"];
						req._authStatus = headers["x-auth-status"];
						if (authHtmlUrl) {
							try {
								authHtmlUrl = decodeURIComponent(authHtmlUrl);
								req._authHtmlUrl = HTTP_RE.test(authHtmlUrl)
									? authHtmlUrl
									: "file://" + authHtmlUrl;
							} catch (e) {}
						} else if (headers.location) {
							req._redirectUrl = headers.location;
						} else if (headers[SHOW_LOGIN_BOX]) {
							req._showLoginBox = true;
						}
					}
				}
				err && logger.error(err);
			}
			if (!forbidden && headers) {
				Object.keys(headers).forEach(function (key) {
					if (key.indexOf("x-whistle-") === 0 || key === "proxy-authorization") {
						var value = headers[key];
						if (
							key === config.WHISTLE_POLICY_HEADER &&
							value === "enableCaptureByAuth"
						) {
							req._forceCapture = true;
						} else if (key === config.CLIENT_ID_HEAD) {
							req._customClientId = value;
						}
						req.headers[key] = value;
						options.headers[key] = value;
					}
				});
			}
			execCallback();
		});
	});
}

function getRulesFromPlugins(type, req, res, callback) {
	var plugins = req.whistlePlugins;
	loadPlugins(plugins, function (ports) {
		ports = ports.map(function (port, i) {
			var plugin = plugins[i];
			return {
				port: port && port[type + "Port"],
				authPort: port && port.authPort,
				plugin: plugin.plugin,
				isRegExp: plugin.isRegExp,
				rawPattern: plugin.rawPattern,
				value: plugin.value,
				url: plugin.url
			};
		});

		var rest = ports.length;
		if (!rest) {
			return callback();
		}

		var results = [];
		var options = getOptions(req, res, type);
		var isResRules = type == "resRules";
		var enableAuth =
			!isResRules &&
			req.justAuth !== false &&
			(!req.isPluginReq || req._isProxyReq) &&
			(!req.fromTunnel || !util.isAuthCapture(req));
		authReq(enableAuth, ports, req, options, function (forbidden) {
			if (forbidden) {
				var noTunnel = !req.isTunnel;
				req._authForbidden = true;
				var mgr = new RulesMgr({ msg: forbidden });
				if (noTunnel && req._authHtmlUrl) {
					mgr.parse(
						"* ignore://!method|!file|!http|!https method://get " + req._authHtmlUrl
					);
				} else if (noTunnel && req._redirectUrl) {
					mgr.parse("* ignore://!redirect redirect://" + req._redirectUrl);
				} else {
					var status = req._authStatus
						? req._showLoginBox
							? noTunnel
								? 401
								: 407
							: 403
						: 502;
					mgr.parse(
						"* ignore://!statusCode|!resBody|!resType|!resCharset status://" +
							status +
							" resBody://{msg} resType://html resCharset://utf8"
					);
				}
				return callback(mgr);
			}
			if (req.justAuth) {
				return callback();
			}
			var hookName = isResRules
				? "RES_RULES"
				: type === "tunnelRules"
					? "TUNNEL_RULES"
					: "REQ_RULES";
			options.headers[PLUGIN_HOOK_NAME_HEADER] = PLUGIN_HOOKS[hookName];
			var execCallback = function () {
				if (--rest <= 0) {
					if (req.resScriptRules) {
						results = results.concat(req.resScriptRules);
					}
					callback(parseRulesList(req, results, isResRules));
				}
			};
			ports.forEach(function (item, i) {
				var plugin = item.plugin;
				if (!item.port) {
					var rulesText = isResRules ? plugin.resRules : plugin._rules;
					if (rulesText) {
						results[i] = {
							text: rulesText,
							values: plugin[util.PLUGIN_VALUES],
							root: plugin.path
						};
					}
					return execCallback();
				}

				var opts = getPluginReqOpts(item, req, options, item.port);
				var cacheKey = plugin.moduleName + "\n" + type;
				var data = rulesCache.get(cacheKey);
				var updateMaxAge = function (obj, age) {
					if (age >= 0) {
						obj.maxAge = age;
						obj.now = Date.now();
					}
				};
				var handleRules = function (err, body, values, raw, res) {
					if (err === false && data) {
						body = data.body;
						values = data.values;
						raw = data.raw;
						updateMaxAge(data, res && res.headers[MAX_AGE_HEADER]);
					} else if (res) {
						var etag = res.headers[ETAG_HEADER];
						var maxAge = res.headers[MAX_AGE_HEADER];
						var newData;
						if (maxAge >= 0) {
							newData = newData || {};
							updateMaxAge(newData, maxAge);
						}
						if (etag) {
							newData = newData || {};
							newData.etag = etag;
						}
						if (newData) {
							newData.body = body;
							newData.values = values;
							newData.raw = raw;
							rulesCache.set(cacheKey, newData);
						} else {
							rulesCache.del(cacheKey);
						}
					}
					var pendingCallbacks = data && data.pendingCallbacks;
					if (pendingCallbacks) {
						delete data.pendingCallbacks;
						pendingCallbacks.forEach(function (cb) {
							cb(err, body, values, raw);
						});
					}
					body = body || "";
					if (isResRules) {
						body += plugin.resRules ? "\n" + plugin.resRules : "";
					} else {
						body += plugin._rules ? "\n" + plugin._rules : "";
					}
					if (body || values) {
						var pluginVals = plugin[util.PLUGIN_VALUES];
						if (values && pluginVals) {
							var vals = extend({}, pluginVals);
							values = extend(vals, values);
						} else {
							values = values || pluginVals;
						}
						results[i] = {
							text: body,
							values: values,
							root: plugin.path
						};
					}
					execCallback();
				};
				delete opts.headers[ETAG_HEADER];
				if (data) {
					if (Date.now() - data.now <= data.maxAge) {
						return handleRules(false);
					}
					if (data.etag) {
						opts.headers[ETAG_HEADER] = data.etag;
					}
					if (data.maxAge >= 0) {
						if (data.pendingCallbacks) {
							data.pendingCallbacks.push(handleRules);
							return;
						}
						data.pendingCallbacks = [];
					}
				}
				opts.ignoreExceedError = true;
				opts.maxLength = MAX_RULES_LENGTH;
				requestRules(opts, handleRules);
			});
		});
	});
}

function getOptions(req, res, type, isPipe) {
	var fullUrl = req.fullUrl || util.getFullUrl(req);
	var options = util.parseUrl(fullUrl);
	var isResRules = res && type === "resRules";
	var headers = extend({}, isResRules ? res.headers : req.headers);
	delete headers.upgrade;
	delete headers.connection;

	options.headers = addRuleHeaders(req, req.rules, headers, isPipe);
	delete headers["content-length"];
	headers[METHOD_HEADER] = encodeURIComponent(req.method || "GET");

	if (isResRules) {
		headers.host = req.headers.host;
		headers[HOST_IP_HEADER] = req.hostIp || LOCALHOST;
		headers[STATUS_CODE_HEADER] = encodeURIComponent(
			res.statusCode == null ? "" : res.statusCode
		);
		if (req.headers.cookie) {
			headers.cookie = req.headers.cookie;
		} else {
			delete headers.cookie;
		}
	}

	if (req.isPluginReq) {
		headers[PLUGIN_REQUEST_HEADER] = 1;
	}

	if (req._existsCustomCert) {
		headers[CUSTOM_CERT_HEADER] = 1;
	}
	if (req._enableCapture) {
		headers[ENABLE_CAPTURE_HEADER] = 1;
	}

	options.protocol = "http:";
	options.host = LOCALHOST;
	options.hostname = null;
	options.agent = false;

	return options;
}

function requestPlugin(options, callback, retryCount) {
	retryCount = retryCount || 0;
	util.request(options, function (err, body, res) {
		if (err && retryCount < 5) {
			return requestPlugin(options, callback, ++retryCount);
		}
		if (res && res.statusCode == 304) {
			return callback(false, null, res);
		}
		callback(null, body && body.trim(), res);
	});
}

function requestRules(options, callback) {
	requestPlugin(options, function (err, body, res) {
		if (err === false) {
			return callback(false, null, null, null, res);
		}
		var rules = body;
		var values = null;
		var data = !err && rulesToJson(body);
		if (data) {
			rules = data.text;
			values = data.values;
		}
		callback(err, rules, values, body, res);
	});
}

function rulesToJson(body) {
	if (body && /^\{[\s\S]+\}$/.test(body)) {
		try {
			body = JSON.parse(body);
			return {
				root: typeof body.root === "string" ? body.root : null,
				text: typeof body.rules === "string" ? body.rules : "",
				values: body.values
			};
		} catch (e) {}
	}
}

function emptyFilter(val) {
	return !!val;
}

function getRulesMgr(type, req, res, callback) {
	var plugins = req.whistlePlugins;
	if (!plugins) {
		return callback();
	}
	getRulesFromPlugins(type, req, res, callback);
}

function getPluginRulesCallback(req, callback) {
	return function (pluginRules) {
		req.pluginRules = pluginRules;
		callback(pluginRules);
	};
}

function resolvePipePlugin(req, callback) {
	if (req._pipePlugin == null) {
		var pipe;
		var hRules = req.headerRulesMgr;
		if (config.multiEnv) {
			pipe = (hRules && hRules.resolvePipe(req)) || rulesMgr.resolvePipe(req);
		} else {
			pipe = rulesMgr.resolvePipe(req) || (hRules && hRules.resolvePipe(req));
		}
		var plugin;
		req._pipeRule = pipe;
		if (pipe && PIPE_PLUGIN_RE.test(pipe.matcher)) {
			req._pipeValue = RegExp.$2;
			plugin = _getPlugin(RegExp.$1 + ":");
		}
		req._pipePlugin = plugin || "";
	}
	loadPlugin(req._pipePlugin, function (_, ports) {
		req._pipePluginPorts = ports || "";
		callback(ports);
	});
}

pluginMgr.resolvePipePlugin = resolvePipePlugin;

function getPipe(type, hookName) {
	var isRes = type.toLowerCase().indexOf("res") !== -1;
	hookName = PLUGIN_HOOKS[hookName];
	return function (req, res, callback) {
		if (!isRes) {
			callback = res;
			res = null;
		}
		resolvePipePlugin(req, function (ports) {
			var port = ports && ports[type + "Port"];
			if (!port || req._hasClosed || req._hasError) {
				return callback();
			}
			var options = getOptions(req, res, isRes && "resRules", true);
			options.headers[PLUGIN_HOOK_NAME_HEADER] = hookName;
			options.headers["x-whistle-request-tunnel-ack"] = 1;
			if (req._isUpgrade) {
				options.headers[UPGRADE_HEADER] = 1;
			}
			options.proxyHost = LOCALHOST;
			options.proxyPort = port;
			delete options.headers[CUSTOM_PARSER_HEADER];
			if (req._websocketExtensions !== null) {
				if (req._websocketExtensions) {
					req.headers["sec-websocket-extensions"] = req._websocketExtensions;
				} else {
					delete req.headers["sec-websocket-extensions"];
				}
			}
			var client;
			var done;
			var handleConnect = function (socket, _res) {
				if (!done) {
					done = true;
					if (req._hasError) {
						return socket.destroy();
					}
					callback(socket);
				}
			};
			var destroy = function () {
				if (client) {
					client.destroy();
					client.socket && client.socket.destroy();
					client = null;
					handleConnect();
				}
			};
			var handleError = function (err) {
				if (client) {
					destroy();
					if (err) {
						debugMode && console.log(req._pipeRule.matcher, type);
						(res || req).emit("error", err);
					}
				}
			};
			client = config.connect(options, handleConnect);
			client.on("error", function () {
				if (client) {
					destroy();
					client = !req._hasError && config.connect(options, handleConnect);
					client && util.onSocketEnd(client, handleError);
				}
			});
			req.once("_closed", function () {
				handleConnect();
				destroy();
			});
		});
	};
}

pluginMgr.getReqReadPipe = getPipe("reqRead", "REQ_READ");
pluginMgr.getReqWritePipe = getPipe("reqWrite", "REQ_WRITE");
pluginMgr.getResReadPipe = getPipe("resRead", "RES_READ");
pluginMgr.getResWritePipe = getPipe("resWrite", "RES_WRITE");

var getWsReqReadPipe = getPipe("wsReqRead", "WS_REQ_READ");
var getWsReqWritePipe = getPipe("wsReqWrite", "WS_REQ_WRITE");
var getWsResReadPipe = getPipe("wsResRead", "WS_RES_READ");
var getWsResWritePipe = getPipe("wsResWrite", "WS_RES_WRITE");
var getTunnelReqReadPipe = getPipe("tunnelReqRead", "TUNNEL_REQ_READ");
var getTunnelReqWritePipe = getPipe("tunnelReqWrite", "TUNNEL_REQ_WRITE");
var getTunnelResReadPipe = getPipe("tunnelResRead", "TUNNEL_RES_READ");
var getTunnelResWritePipe = getPipe("tunnelResWrite", "TUNNEL_RES_WRITE");

pluginMgr.getWsPipe = function (req, res, callback) {
	req._websocketExtensions = res.headers["sec-websocket-extensions"] || "";
	getWsReqReadPipe(req, function (reqRead) {
		getWsReqWritePipe(req, function (reqWrite) {
			getWsResReadPipe(req, res, function (resReadStream) {
				getWsResWritePipe(req, res, function (resWriteStream) {
					callback(reqRead, reqWrite, resReadStream, resWriteStream);
				});
			});
		});
	});
};

pluginMgr.getTunnelPipe = function (req, res, callback) {
	getTunnelReqReadPipe(req, function (reqRead) {
		getTunnelReqWritePipe(req, function (reqWrite) {
			getTunnelResReadPipe(req, res, function (resRead) {
				getTunnelResWritePipe(req, res, function (resWrite) {
					callback(reqRead, reqWrite, resRead, resWrite);
				});
			});
		});
	});
};

pluginMgr.getRules = function (req, callback) {
	getRulesMgr("rules", req, null, getPluginRulesCallback(req, callback));
};

pluginMgr.getResRules = function (req, res, callback) {
	req.curUrl = req.fullUrl;
	if (!req.resHeaders && res) {
		req.resHeaders = res.headers;
	}
	var resRules = rulesMgr.resolveResRules(req, true);
	var pRules = req.pluginRules && req.pluginRules.resolveResRules(req, true);
	var fRules = req.rulesFileMgr && req.rulesFileMgr.resolveResRules(req, true);
	var hRules = req.headerRulesMgr && req.headerRulesMgr.resolveResRules(req, true);
	fRules && util.mergeRules(req, fRules, true);
	config.multiEnv && util.mergeRules(req, resRules, true);
	hRules && util.mergeRules(req, hRules, true);
	pRules && util.mergeRules(req, pRules, true);
	!config.multiEnv && util.mergeRules(req, resRules, true);
	var resScriptRules;
	var resHeaderRules = res.headers[config.RES_RULES_HEAD];
	if (resHeaderRules) {
		try {
			resHeaderRules = rulesToJson(decodeURIComponent(resHeaderRules));
			if (resHeaderRules) {
				resScriptRules = resScriptRules || [];
				resScriptRules.push(resHeaderRules);
			}
		} catch (e) {}
	}
	delete res.headers[config.RES_RULES_HEAD];
	rulesMgr.resolveResRulesFile(req, res, function (result) {
		if (result) {
			resScriptRules = resScriptRules || [];
			resScriptRules.push(result);
		}
		req.resScriptRules = resScriptRules;
		getRulesMgr("resRules", req, res, function (pluginRulesMgr) {
			if (!pluginRulesMgr && resScriptRules) {
				pluginRulesMgr = parseRulesList(req, resScriptRules, true);
			}
			req.resScriptRules = resScriptRules = null;
			callback(pluginRulesMgr);
		});
	});
};

pluginMgr.getTunnelRules = function (req, callback) {
	getRulesMgr("tunnelRules", req, null, getPluginRulesCallback(req, callback));
};

function postStats(req, res) {
	var plugins = req.whistlePlugins;
	var type = res ? "_postResStats" : "_postReqStats";
	if (!plugins || req.isPluginReq || req[type]) {
		return;
	}
	req[type] = true;
	loadPlugins(plugins, function (ports) {
		ports = ports
			.map(function (port, i) {
				var plugin = plugins[i];
				var statsPort = port && (res ? port.resStatsPort : port.statsPort);
				if (!statsPort) {
					return;
				}
				return {
					plugin: plugin.plugin,
					port: statsPort,
					value: plugin.value,
					url: plugin.url
				};
			})
			.filter(emptyFilter);

		if (!ports.length) {
			return;
		}
		var options = getOptions(req, res, "resRules");
		options.headers[PLUGIN_HOOK_NAME_HEADER] = PLUGIN_HOOKS[res ? "RES_STATS" : "REQ_STATS"];
		ports.forEach(function (item) {
			var opts = extend({}, options);
			opts.headers = extend({}, options.headers);
			opts.port = item.port;
			addPluginVars(req, opts.headers, item);
			var request = http.request(opts, function (response) {
				response.on("error", util.noop);
				response.on("data", util.noop);
			});
			request.on("error", util.noop);
			request.end();
		});
	});
}

pluginMgr.postStats = postStats;

var PLUGIN_RULE_RE = /^([a-z\d_\-]+)(?:\(([\s\S]*)\))?$/;
var PLUGIN_RULE_RE2 = /^(?:\w+\.)?([a-z\d_\-]+)(?:\:\/\/([\s\S]*))?$/;
var PLUGIN_RE = /^plugin:\/\//;

function getPluginByPluginRule(pluginRule) {
	if (!pluginRule) {
		return;
	}

	var value = pluginRule.matcher;
	if (PLUGIN_RE.test(value)) {
		value = util.getMatcherValue(pluginRule);
	} else {
		value = util.rule.getMatcher(pluginRule);
	}

	if (PLUGIN_RULE_RE.test(value) || PLUGIN_RULE_RE2.test(value)) {
		value = RegExp.$2;
		var plugin = _getPlugin(RegExp.$1 + ":");
		if (!plugin) {
			return;
		}
		var ruleUrl = util.getUrlValue(pluginRule, true);
		return (
			plugin && {
				plugin: plugin,
				value: value,
				url: ruleUrl == value ? undefined : ruleUrl
			}
		);
	}
}

function resolveWhistlePlugins(req) {
	var rules = req.rules;
	var plugins = [];
	var plugin = (req.pluginMgr = getPluginByRuleUrl(util.rule.getUrl(rules.rule)));
	if (plugin) {
		rules._pluginRule = rules.rule;
		var ruleValue = util.getMatcherValue(rules.rule);
		var ruleUrl = util.getUrlValue(rules.rule, true);
		plugins.push({
			plugin: plugin,
			value: ruleValue,
			isRegExp: rules.rule.isRegExp,
			rawPattern: rules.rule.rawPattern,
			url: ruleUrl == ruleValue ? undefined : ruleUrl
		});
	}
	if (rules.plugin) {
		var _plugins = [plugin];
		rules.plugin.list.forEach(function (rule) {
			var info = getPluginByPluginRule(rule);
			if (info && _plugins.indexOf(info.plugin) == -1) {
				info.isRegExp = rule.isRegExp;
				info.rawPattern = rule.rawPattern;
				_plugins.push(info.plugin);
				plugins.push(info);
			}
		});
	}
	if (plugins.length) {
		req.whistlePlugins = plugins;
	}
	return plugin;
}

pluginMgr.resolveWhistlePlugins = resolveWhistlePlugins;

pluginMgr.updatePluginRules = function (name) {
	name && httpMgr.forceUpdate(name + "/");
};

pluginMgr.setProxy = function (p) {
	whistleProxy = p;
};

httpMgr.setPluginMgr(pluginMgr);

var PLUGIN_KEY_RE = /^\$(?:whistle\.)?([a-z\d_-]+)[/:]([\S\s]+)$/;
var MAX_VALUE_LEN = 1024 * 1024 * 16;
var MAX_URL_VAL_LEN = 1024 * 256;

function requestValue(options, callback, isBin) {
	options.needRawData = isBin;
	var handleCallback = function (err, body, res) {
		var code = res && res.statusCode;
		if (code != 200) {
			body = "";
			if (!err) {
				err = new Error("Error: response " + code);
				err.code = code || 500;
			}
		}
		err && logger.error(err);
		callback(body, err, res);
	};
	httpMgr.request(extend({}, options), function (err, body, res) {
		if (err) {
			return httpMgr.request(options, handleCallback);
		}
		handleCallback(err, body, res);
	});
	return options;
}

pluginMgr.resolveKey = function (url, rule, req) {
	if (HTTP_RE.test(url)) {
		return {
			url: url,
			originalKey: url,
			maxLength: MAX_URL_VAL_LEN
		};
	}
	if (!PLUGIN_KEY_RE.test(url)) {
		return;
	}
	var name = RegExp.$1;
	var key = RegExp.$2;
	if (!getActivePluginByName(name)) {
		return;
	}
	var ruleName = protocols.getRuleProto(rule);
	var headers = extend({}, req && req.headers, config.pluginHeaders);
	if (req) {
		addPluginHeaders(req, headers, true);
		headers[METHOD_HEADER] = req.method ? encodeURIComponent(req.method) : "GET";
		delete headers.upgrade;
		delete headers.connection;
		delete headers["content-length"];
	}
	if (ruleName) {
		headers["x-whistle-rule-proto"] = ruleName;
	}
	return {
		originalKey: url,
		pluginName: name,
		maxLength: MAX_VALUE_LEN,
		url: name + "/api/key/value?key=" + util.encodeURIComponent(key),
		headers: headers
	};
};

pluginMgr.requestText = function (options, callback) {
	return requestValue(options, callback);
};

pluginMgr.requestBin = function (options, callback) {
	return requestValue(options, callback, true);
};

util.setPluginMgr(pluginMgr);

module.exports = pluginMgr;

RulesMgr.setPluginMgr(pluginMgr);
